// This .NET C# script inserts Apache HTTP Server's access log records into database
// Copyright (c) Iaroslav Vassiliev, 2013
// Distributed under GNU General Public License version 2 - http://www.gnu.org/licenses/gpl.html
// .NET Framework 3.5 or higher is required to compile this script

//#define MULTI_THREADING		// enable multi-threading using ThreadPool, experimental
#if MULTI_THREADING
    using System.Threading;
    #warning MULTI_THREADING is enabled
#endif

using System;
using System.Data.SqlClient;
using System.Globalization;
using System.IO;
using System.Reflection;
using System.Xml.Serialization;
using System.Text;
using System.Text.RegularExpressions;

using MySql.Data;   // requires referencing mysql.data.dll (MySQL Connector/Net)                   
using MySql.Data.MySqlClient;   // see http://dev.mysql.com/downloads/connector/net/1.0.html

[assembly: AssemblyTitle("ApacheLogToDB")]
[assembly: AssemblyDescription("This program inserts Apache HTTP Server's log records into database")]
[assembly: AssemblyProduct("ApacheLogToDB")]
[assembly: AssemblyCopyright("Copyright (GNU GPL 2) (c) Iaroslav Vassiliev <codedriller@gmail.com>, 2013")]
[assembly: AssemblyVersionAttribute("1.3.0.0")]
[assembly: AssemblyFileVersion("1.3.0.0")]

/// <summary>Configuration settings to be loaded from XML file.</summary>
[Serializable]
public class Config
{
    /// <summary>Type of database to which to connect: mysql (for MySQL) or sqlsrv (for Microsoft
    /// SQL Server). Defaults is sqlsrv.</summary>
    public string dbType = "sqlsrv";    // { get; set; }
    /// <summary>Name of the database instance to which to connect. Default is localhost.</summary>
    public string dbHost = "localhost";
    /// <summary>Name of the database to insert log records into.</summary>
    public string dbName = "";
    /// <summary>Name of the table to insert log records into.</summary>
    public string dbTable = "";
    /// <summary>Login of the database user. That user must have sufficient rights to create table,
    /// to write data into the table, and in case of SQL Server to use dbo scheme.</summary>
    public string dbUser = "";
    /// <summary>Database user's password.</summary>
    public string dbPassword = "";
    /// <summary>Number of seconds to wait while trying to establish a connection
    /// before terminating the attempt and generating an error. Default is 10.</summary>
    public int dbConnectTimeout = 10;

    /// <summary>If any log record's parameter exceeds this number of characters,
    /// the overhead will be cut off. Default is 255. If maxParamLength is set to 0,
    /// no cutting occurs.</summary>
    public int maxParamLength = 255;

    /// <summary>The path to and the name of the file, to which ApacheLogToDB will record
    /// it's internal errors, e.g. C:\Logs\ApacheLogToDb.error.log. If it's not specified, the file
    /// ApacheLogToDb.error.log will be automatically created in Apache's \bin directory.</summary>
    public string errorFile = "";
    /// <summary>The path to and the name of the file, to which ApacheLogToDB will dump all
    /// records received from Apache, e.g. C:\Logs\ApacheLogToDb.dump.log. If it's not specified,
    /// no dumping will occur. It's useful for debugging purposes only.</summary>
    public string dumpFile = "";
    /// <summary>The path to and the name of the file, to which ApacheLogToDB will record
    /// profiling information. It's useful for debugging purposes only.</summary>
    public string profilingFile = "";
    /// <summary>If maxParamLength limit is specified, every log record's parameter exceeding
    /// that limit will be truncated. Such truncated records can be saved in file.</summary>
    public string longQueriesFile = "";

    /// <summary>
    /// Apache's log format.
    /// To conform to ApacheLogToDB's requirements, log format string must begin with "|"
    /// character and must contain only the Apache's log format arguments, delimited by "|"
    /// character. No other characters are allowed. Here are some examples:
    /// "|%h|%l|%u|%t|%r|%>s|%b"
    /// "|%h|%l|%t|%r|%s|%b|%{Referer}i|%{User-agent}i"
    /// "|%t|%v|%{REMOTE_USER}e|%a|%{X-Forwarded-For}i|%m|%U|%q|%s|%B|%D|%{dds_auth}C"
    /// See http://httpd.apache.org/docs/current/mod/mod_log_config.html for arguments details.
    /// The same string must be specified in LogFormat directive in Apache's configuration file.
    /// This setting is very important. Please, specify it carefully.
    /// </summary>
    public string apacheLogFormat = "|%h|%l|%u|%t|%r|%>s|%b";

    /// <summary>
    /// The format of timestamp (%t), that Apache uses in it's log.
    /// This setting can usually be omitted.
    /// Default is [dd/MMM/yyyy:HH:mm:ss zz00] and normally it doesn't change unless a user
    /// does change it explicitly in Apache's configuration file.
    /// Use .NET's standard date and time format strings to describe new timestamp here if needed.
    /// MSDN reference is here: http://msdn.microsoft.com/en-us/library/az4se3k1(v=vs.90).aspx
    /// </summary>
    public string apacheTimestampFormat = "[dd/MMM/yyyy:HH:mm:ss zz00]";
}

class ApacheLogToDB
{
    private static Config config;
    private static string configFile = "ApacheLogToDB.conf.xml";
    private static string errorFile = "ApacheLogToDB.error.log";

    private static string errorLogTimestampFormat = "dd MMM yyyy HH:mm:ss";
    private static CultureInfo enUS = new CultureInfo("en-US");
    //private static DateTime progStartTime = DateTime.Now;
    private static int insertionsCount = 0;
    private static int insertionsLimit = 0;
	private static char[] delim = new char [] { '|' };
    private static DateTime profilingTime;
    private static SqlConnection sqlSrvConn = null;
    private static MySqlConnection mySqlConn = null;
	private static string pureLogFormat = "";
	private static Regex logFormatRegex;

    /// <summary>Will contain string with types of all log parameters, e.g. t,s,B,b</summary>
    private static string[] paramFormat;

    /// <summary>
    /// This array contains Apache's log format constants, their short descriptions and their
    /// according SQL data types (firstly SQL server, secondly MySQL).
    /// The digit 0/1 denotes if format string can contain
    /// additional string in {} braces, e.g. %{VARNAME}e.
    /// Apache 2.4 documentation was used for reference:
    /// http://httpd.apache.org/docs/2.4/mod/mod_log_config.html
    /// NOTE: &L and &P can be numeric, but are denoted as strings for safety.
    /// NOTE: There can be %>s format string, also numeric.
    /// </summary>
    private static string[,] afc = new string[,] {
        { "A","0","Local_IP","varchar(255)","VARCHAR(255) ASCII" },
        { "a","1","Client_IP","varchar(255)","VARCHAR(255) ASCII" },
        { "B","0","Response_bytes","int","INT" },
        { "b","0","Response_bytes_CLF","int","INT" },
        { "C","1","Cookie","nvarchar(255)","VARCHAR(255) CHARACTER SET utf8" },
        { "D","0","Serving_microseconds","int","INT" },
        { "e","1","Environment_variable","nvarchar(255)","VARCHAR(255) CHARACTER SET utf8" },
        { "f","0","Filename","varchar(255)","VARCHAR(255) ASCII" },
        { "h","0","Remote_host","varchar(255)","VARCHAR(255) ASCII" },
        { "H","0","Request_protocol","varchar(255)","VARCHAR(255) ASCII" },
        { "i","1","Request_header","nvarchar(255)","VARCHAR(255) CHARACTER SET utf8" },
        { "k","0","Keepalive_requests","int","INT" },
        { "l","0","Remote_logname","varchar(255)","VARCHAR(255) ASCII" },
        { "L","0","ID_from_error_log","varchar(255)","VARCHAR(255) ASCII" },
        { "m","0","Request_method","varchar(255)","VARCHAR(255) ASCII" },
        { "n","1","Note","nvarchar(255)","VARCHAR(255) CHARACTER SET utf8" },
        { "o","1","Reply_header","varchar(255)","VARCHAR(255) ASCII" },
        { "P","1","Process_ID","varchar(255)","VARCHAR(255) ASCII" },
        { "p","1","Port","int","INT" },
        { "q","0","Query_string","varchar(255)","VARCHAR(255) ASCII" },
        { "R","0","Handler","varchar(255)","VARCHAR(255) ASCII" },
        { "r","0","First_request_line","varchar(255)","VARCHAR(255) ASCII" },
        { "s","0","Original_Status","smallint","SMALLINT" },
        { ">s","0","Final_status","smallint","SMALLINT" },
        { "T","0","Serving_seconds","int","INT" },
        { "t","1","Timestamp","datetime","TIMESTAMP" },
        { "U","0","URL_path","varchar(255)","VARCHAR(255) ASCII" },
        { "u","0","Remote_user","nvarchar(255)","VARCHAR(255) CHARACTER SET utf8" },
        { "V","0","Canonical_Server_Name","varchar(255)","VARCHAR(255) ASCII" },
        { "v","0","Server_Name","varchar(255)","VARCHAR(255) CHARACTER SET utf8" },
        { "X","0","Keepalive_status","char(1)","CHAR(1) ASCII" },
        { "I","0","Bytes_received","int","INT" },
        { "O","0","Bytes_sent","int","INT" }
    };
    private static char[] numericAfc = new char[] { 'B', 'b', 'D', 'k', 'p', 's', 'T', 'I', 'O' };
    private static char[] stringAfc = new char[] { 'A', 'a', 'C', 'e', 'f', 'H', 'h', 'i', 'L',
        'l', 'm', 'n', 'o', 'P', 'q', 'R', 'r', 'U', 'u', 'V', 'v', 'X' };

    static void LoadConfig()
    {
        string currentDir = Directory.GetCurrentDirectory();
        char dirSep = Path.DirectorySeparatorChar;

        if( !File.Exists( configFile ) )
        {
            // it's not always clear if current direcory is "apache" or "apache\bin"
            if (!File.Exists(currentDir + dirSep + "bin" + dirSep + configFile))
            {
                string msg = "Could not find configuration file: " +
                    currentDir + dirSep + configFile;
                LogError(msg);
                throw new FileNotFoundException(msg, configFile);
            }
            configFile = currentDir + dirSep + "bin" + dirSep + configFile;
        }

        var serializer = new XmlSerializer( typeof(Config) );
        using( var f = File.OpenRead(configFile) )
            config = serializer.Deserialize(f) as Config;

        if( config.errorFile != "" )
            errorFile = config.errorFile;

        if (config.dbType != "sqlsrv" && config.dbType != "mysql")
        {
            string msg = "Unsupported database type: " + config.dbType;
            LogError(msg);
            throw new ArgumentOutOfRangeException("dbType", msg);
        }

        if( config.dbName == "" )
        {
            string msg = "Required configuration parameter is missing: dbName";
            LogError( msg );
            throw new ArgumentNullException( "dbName", msg );
        }
        if( config.dbTable == "" )
        {
            string msg = "Required configuration parameter is missing: dbTable";
            LogError( msg );
            throw new ArgumentNullException( "dbTable", msg );
        }
        if( config.dbUser == "" )
        {
            string msg = "Required configuration parameter is missing: dbUser";
            LogError( msg );
            throw new ArgumentNullException( "dbUser", msg );
        }
        if( config.dbPassword == "" )
        {
            string msg = "Required configuration parameter is missing: dbPassword";
            LogError( msg );
            throw new ArgumentNullException( "dbPassword", msg );
        }

        // paramFormat array will contain types of log parameters
        pureLogFormat = config.apacheLogFormat.Replace(" ", "").Trim();
        pureLogFormat = Regex.Replace(pureLogFormat, "%{[^}]*}", "");
        pureLogFormat = pureLogFormat.Replace("%", "");
        paramFormat = pureLogFormat.Replace(">", "").Split(delim,
            StringSplitOptions.RemoveEmptyEntries);

        // Check paramFormat elements
        foreach (string param in paramFormat)
        {
            if (param.Length > 1 || (param != "t"
                && Array.IndexOf(numericAfc, param[0]) == -1
                && Array.IndexOf(stringAfc, param[0]) == -1))
            {
                string msg = "Unrecognized identifier in apacheLogFormat configuration " +
                    "setting: " + param + '\n' +
                    "apacheLogFormat: " + config.apacheLogFormat;
                LogError(msg);
                throw new ArgumentOutOfRangeException("apacheLogFormat", msg);
            }
        }

		// Make Regex
		string regexStr = "(?s)^";
		foreach (string param in paramFormat)
		{
			if( Array.IndexOf(stringAfc, param[0]) != -1 )
			{
				regexStr += @"\|(.*)";
			}
			else if( Array.IndexOf(numericAfc, param[0]) != -1 )
			{
				regexStr += @"\|([-\d\.]*)";
			}
			else if( param == "t" )
			{
				if( config.apacheTimestampFormat == "[dd/MMM/yyyy:HH:mm:ss zz00]" )
				{
					regexStr += @"\|(\[\d\d/[A-Za-z]{3}/\d{4}:\d\d:\d\d:\d\d .?\d\d00])";
				}
				else
				{
					regexStr += @"\|(.*)";
				}
			}
		}
		logFormatRegex = new Regex( regexStr + '$' );

        // Adjust maxParamLength in SQL types
        if (config.maxParamLength != 255)
        {
            for (int i = 0; i < afc.GetLength(0); i++)
            {
                for (int j = 0; j < afc.GetLength(1); j++)
                {
                    if (config.maxParamLength != 0)
                    {
                        afc[i, j] = afc[i, j].Replace("255", config.maxParamLength.ToString());
                        continue;
                    }

                    if (afc[i,j] == "varchar(255)" || afc[i,j] == "nvarchar(255)")
                    {
                        afc[i, j] = afc[i, j].Replace("255", "max");
                    }
                    else if(afc[i,j] == "VARCHAR(255) ASCII")

                    {
                        afc[i, j] = afc[i, j].Replace("255", "4000");   // MySQL max rowsize is 65535 bytes
                    }
                    else if (afc[i, j] == "VARCHAR(255) CHARACTER SET utf8")
                    {
                        afc[i, j] = afc[i, j].Replace("255", "4000");   // MySQL max rowsize is 65535 bytes
                    }
                }
            }
        }
    }

    public static void ConnectToSqlServerDb()
    {
        if (config.dbHost.IndexOf(':') != -1)
        {
            config.dbHost = config.dbHost.Replace(':', ',');
        }

        string sqlConnStr =     // http://msdn.microsoft.com/en-us/library/system.data.sqlclient.sqlconnection.connectionstring.aspx
            "Data Source=" + config.dbHost + ';' +
            "Initial Catalog=" + config.dbName + ';' +
            "User ID=" + config.dbUser + ';' +
            "Password=" + config.dbPassword + ';' +
            "Connect Timeout=" + config.dbConnectTimeout + ';';

        try
        {
            sqlSrvConn = new SqlConnection(sqlConnStr);
        }
        catch (Exception e)
        {
            LogError("Could not initialize database connection with the following parameters:\n" +
                sqlConnStr + "\n\n" + "Exception caught: " + e.Message);
            throw e;
        }
    }

    public static void ConnectToMySqlDb()
    {
        if (config.dbHost.IndexOf(':') != -1)
        {
            config.dbHost = config.dbHost.Replace(":", ";Port=");
        }

        string sqlConnStr =   // http://dev.mysql.com/doc/refman/5.1/en/connector-net-connection-options.html
            "Server=" + config.dbHost + ';' +
            "Database=" + config.dbName + ';' +
            "User=" + config.dbUser + ';' +
            "Password=" + config.dbPassword + ';' +
            "ConnectionTimeout=" + config.dbConnectTimeout + ';';

        try
        {
            mySqlConn = new MySqlConnection(sqlConnStr);
        }
        catch (Exception e)
        {
            LogError("Could not initialize database connection with the following parameters:\n" +
                sqlConnStr + "\n\n" + "Exception caught: " + e.Message);
            throw e;
        }
    }

    /// <summary>
    /// Create database table to store log data according to apacheLogFormat configuration setting.
    /// To invoke this method ApacheLogToDB program must be called
    /// with -t command-line parameter.
    /// NOTE: After the creation, table structure should be carefully reviewed and modified if
    /// necessary before actually using the table to store log data.
    /// </summary>
    public static void CreateSqlServerDbTable()
    {
        Console.WriteLine("Apache's log format string: " + config.apacheLogFormat);
        Console.WriteLine("Creating database table according to the specified format...");
        string sql = "CREATE TABLE [" + config.dbName + "].[dbo].[" + config.dbTable + "] (";
        foreach (string param in pureLogFormat.Split(delim, StringSplitOptions.RemoveEmptyEntries))
        {
            for (int i = 0; i < afc.GetLength(0); i++)
            {
                if (param == afc[i, 0])
                {
                    sql += "\n\t" + afc[i, 2] + ' ' + afc[i, 3] + " NOT NULL,";
                }
            }
        }
        sql = sql.Remove(sql.Length-1) + "\n);";

        try
        {
            SqlCommand sqlQuery = new SqlCommand();
            sqlQuery.Connection = sqlSrvConn;
            if (sqlSrvConn.State != System.Data.ConnectionState.Open)
                sqlSrvConn.Open();
            sqlQuery.CommandText = sql;
            sqlQuery.ExecuteNonQuery();
        }
        catch (Exception e)
        {
            LogError("Could not create " + config.dbName + "." + config.dbTable +
                " database table" + "\n\n" + "Exception has arised: " + e.Message);
            throw e;
        }
        Console.WriteLine("Database table " + config.dbName + "." + config.dbTable +
            " was created successfully");
    }

    /// <summary>
    /// Create database table to store log data according to apacheLogFormat configuration setting.
    /// To invoke this method ApacheLogToDB program must be called
    /// with -t command-line parameter.
    /// NOTE: After the creation, table structure should be carefully reviewed and modified if
    /// necessary before actually using the table to store log data.
    /// </summary>
    public static void CreateMySqlDbTable()
    {
        Console.WriteLine("Apache's log format string: " + config.apacheLogFormat);
        Console.WriteLine("Creating database table according to the specified format...");
        string sql = "CREATE TABLE " + config.dbTable + " (";
        foreach (string param in pureLogFormat.Split(delim, StringSplitOptions.RemoveEmptyEntries))
        {
            for (int i = 0; i < afc.GetLength(0); i++)
            {
                if (param == afc[i, 0])
                {
                    sql += "\n\t" + afc[i, 2] + ' ' + afc[i, 4] + ',';
                }
            }
        }
        sql = sql.Remove(sql.Length - 1) + "\n);";

        try
        {
            mySqlConn.Open();
            MySqlCommand sqlQuery = new MySqlCommand(sql, mySqlConn);
            sqlQuery.ExecuteNonQuery();
        }
        catch (Exception e)
        {
            LogError("Could not create " + config.dbTable + " table in " + config.dbName +
                " database." + "\n\n" + "Exception has arised: " + e.Message);
            throw e;
        }
        Console.WriteLine("Database table " + config.dbTable + " was created successfully");
    }

    /// <summary>
    /// Used to generate configuration XML file template. Call the program with -g
    /// command-line parameter to invoke this method.
    /// </summary>
    public static void MakeConfigFileTemplate()
    {
        var configTemplate = new Config();
        var templateSerializer = new XmlSerializer( typeof(Config) );
        using( var f = File.OpenWrite( "ApacheLogToDB.conf.template.xml" ) )
            templateSerializer.Serialize( f, configTemplate );
        Console.WriteLine( "ApacheLogToDB.conf.template.xml file generated successfully" );
    }

    public static void LogError( string msg )
    {
        File.AppendAllText( errorFile,
            DateTime.Now.ToString( errorLogTimestampFormat, enUS ) + "\n\n" + msg +
            "\n\n--------------------\n\n" );
    }

	/// <summary>The entry point function.</summary>
    public static void Main( string[] args )
	{
        string createDbTable = "no";

        foreach( string arg in args )
        {
            if (arg.Contains("-genConfigFileTemplate"))
            {
                MakeConfigFileTemplate();
                return;
            }
            else if (arg.Contains("-createTableAndExit"))
            {
                createDbTable = "yes";
            }
            else if (arg.Contains("-createTableOnTheFly"))
            {
                createDbTable = "on-the-fly";
            }
        }

        LoadConfig();

        if (config.dbType == "sqlsrv")
        {
            ConnectToSqlServerDb();
            if (createDbTable != "no")
            {
                CreateSqlServerDbTable();
                if (createDbTable != "on-the-fly")
                {
                    sqlSrvConn.Close();
                    return;
                }
            }
        }
        else if (config.dbType == "mysql")
        {
            ConnectToMySqlDb();
            if (createDbTable != "no")
            {
                CreateMySqlDbTable();
                if (createDbTable != "on-the-fly")
                {
                    mySqlConn.Close();
                    return;
                }
            }
        }

		string line = "";
		while( (line = Console.In.ReadLine()) != null )
		{
			if( line == "" || line[0] != '|' )
				continue;

            if( config.dumpFile != "" )
                File.AppendAllText( config.dumpFile, line + "\n\n" );

		  #if MULTI_THREADING
			ThreadPool.QueueUserWorkItem( ParseAndInsertRecordIntoDb, line );
          #else
            if( config.profilingFile != "" )
                profilingTime = DateTime.Now;
            ParseAndInsertRecordIntoDb( line );
		  #endif

            insertionsCount++;
            if (insertionsLimit > 0 && insertionsCount == insertionsLimit)
                break;
		}
	}

    /// <summary>Function parses log record received from Apache and inserts it into database.</summary>
	public static void ParseAndInsertRecordIntoDb( Object lineObj )
	{
		string line = (string) lineObj;

        bool longQueryRecorded = false;

		try {
			// remove first '|' char and then split params
            string[] param = line.Substring(1).Split( delim, StringSplitOptions.None );

			// check number of params
            if( param.Length != paramFormat.Length )
            {
				// try parsing with regex as a fallback method
				Match m = logFormatRegex.Match(line);
				if( m.Success && m.Groups.Count == paramFormat.Length + 1 )
				{
					param = new string[paramFormat.Length];
					for (int i = 0; i < paramFormat.Length; i++)
					{
						param[i] = m.Groups[i+1].Value;
					}
				}
				else
				{
					string msg = "The log record received from Apache doesn't match the format, " +
						"that was specified in apacheLogFormat configuration setting." + '\n' +
						"apacheLogFormat: " + config.apacheLogFormat + '\n' +
						"received record: " + line;
					LogError( msg );
					return;    // just skip wrong record and continue
					//throw new ArgumentOutOfRangeException( "apacheLogFormat", msg );
				}
			}

            for( int i = 0; i < paramFormat.Length; i++ )
            {
                if( config.maxParamLength > 0 && param[i].Length > config.maxParamLength )
                {
                    param[i] = param[i].Remove(config.maxParamLength);
                    if( config.longQueriesFile != "" && !longQueryRecorded )
                    {
                        File.AppendAllText( config.longQueriesFile,
                            DateTime.Now.ToString(errorLogTimestampFormat, enUS) + "\n\n" + line +
                            "\n\n--------------------\n\n" );
                        longQueryRecorded = true;
                    }
                }

                if (paramFormat[i] == "t")    // timestamp
                {
                    DateTime timestamp;
					if (!DateTime.TryParseExact(param[i], config.apacheTimestampFormat,
                        enUS, DateTimeStyles.None, out timestamp))
                    {
                        string msg = "The timestamp received from Apache doesn't match the " +
                            "format, that was specified in apacheTimestampFormat configuration " +
                            "setting." + '\n' +
                            "apacheTimestampFormat setting: " + config.apacheTimestampFormat + '\n' +
                            "received timestamp: " + param[i];
                        //return;
                        throw new ArgumentOutOfRangeException("apacheTimestampFormat", msg);
                    }
					param[i] = '\'' + timestamp.ToString("s") + '\'';    // 2008-06-15T21:15:07
                }
                else if (Array.IndexOf(numericAfc, paramFormat[i][0]) != -1)    // integer
                {
					if (param[i] == "-" || param[i] == "")
                    {
                        param[i] = "0";
                    }
                    else
                    {
                        int n = 0;
                        if (!int.TryParse(param[i], out n))
                        {
                            string msg = "The number received from Apache couldn't be " +
                                "recognized. Please, review apacheLogFormat configuration " +
                                "setting." + '\n' +
                                "Received number: " + param[i] + '\n' +
                                "Expected Apache's format: %" + paramFormat[i];
                            //return;
                            throw new ArgumentOutOfRangeException("apacheLogFormat", msg);
                        }
                        param[i] = n.ToString();
                    }
                }
                else    // it's string
                {
                    if (config.dbType == "sqlsrv")
					{
                        if (param[i].IndexOf('\'') != -1)
                            param[i] = param[i].Replace("'", "''");
                        // no other escaping seems to be necessary

                    }
                    if (config.dbType == "mysql")
					{
                        param[i] = '\'' + MySqlHelper.EscapeString(param[i]) + '\'';
                    }
                }
            }

            StringBuilder sql = new StringBuilder();
            for (int i = 0; i < paramFormat.Length; i++)
            {
                if (i != 0)
                    sql.Append(',');
                sql.Append(param[i]);
            }

            if (config.dbType == "sqlsrv")
            {
                SqlCommand sqlQuery = new SqlCommand();
                sqlQuery.Connection = sqlSrvConn;
                sqlSrvConn.Open();     // connection pooling is enabled by default
                sqlQuery.CommandText = "INSERT INTO [" + config.dbName + "].[dbo].[" +
                    config.dbTable + "] VALUES (" + sql.ToString() + ')';
                if (sqlQuery.ExecuteNonQuery() != 1)   // exactly one row must be inserted
                {
                    string msg = "SQL query failed: " + sqlQuery.CommandText + '\n' +
                        "Source record: " + line;
                    sqlSrvConn.Close();
                    throw new ArgumentOutOfRangeException("sqlQuery", msg);
                }
                sqlSrvConn.Close();
            }
            else if (config.dbType == "mysql")
            {
                mySqlConn.Open();
                MySqlCommand sqlQuery = new MySqlCommand("INSERT INTO " + config.dbTable +
                    " VALUES (" + sql + ");", mySqlConn);
                if (sqlQuery.ExecuteNonQuery() != 1)   // exactly one row must be inserted
                {
                    string msg = "SQL query failed: " + sqlQuery.CommandText + '\n' +
                        "Source record: " + line;
                    mySqlConn.Close();
                    throw new ArgumentOutOfRangeException("sqlQuery", msg);
                }
                mySqlConn.Close();
            }


          #if !MULTI_THREADING
            if( config.profilingFile != "" )
                File.AppendAllText( config.profilingFile, "Log record insertion time: " +
                    (DateTime.Now - profilingTime).TotalMilliseconds.ToString() + '\n' );
          #endif

		}
		catch ( Exception e ) {
			string msg = "Exception caught: " + e.Message + '\n' +
			    "StdIn line: " + line + '\n' +
				"StackTrace: " + e.StackTrace;
            LogError( msg );
            throw e;
		}
	}
}